package main

import (
	"bufio"
	"bytes"
	"fmt"
	"gin/internal/app/common/util"
	"golang.org/x/net/ipv4"
	"io"
	"log"
	"math"
	"net"
	"net/http"
	"os"
	"os/signal"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"sync"
	"syscall"
	"time"
)

var (
	validURLs      []PlaylistItem // 所有已验证有效的频道项
	validURLsMutex sync.Mutex     // 并发安全访问
)

const (
	numWorkers = 5
	urlBatch   = 10
)

type Config struct {
	Type string
	URL  string
	Path string
}

// PlaylistItem 表示一个频道项
type PlaylistItem struct {
	Name  string
	URL   string
	Group string // 可选字段，用于分组信息
}

type IndexedURL struct {
	URL   string
	Index int
}

func main() {

	configType, err := util.GetConfigValue("iptv.type")
	var iptvtype string
	if err != nil {
		fmt.Printf("Error: %v\n", err)
	} else {
		if configType != nil {
			iptvtype = configType.(string)
		}
	}
	configUrl, err := util.GetConfigValue("iptv.url")
	var iptvurl string
	if err != nil {
		fmt.Printf("Error: %v\n", err)
	} else {
		if configUrl != nil {
			iptvurl = configUrl.(string)
		}
	}

	configPath, err := util.GetConfigValue("iptv.path")
	var iptvpath string
	if err != nil {
		fmt.Printf("Error: %v\n", err)
	} else {
		if configPath != nil {
			iptvpath = configPath.(string)
		}
	}

	config := Config{
		Type: iptvtype,
		URL:  iptvurl,
		Path: iptvpath,
	}

	var items []PlaylistItem

	switch config.Type {
	case "blind":
		urls := generateBlindURLs(config.URL)
		for i, url := range urls {
			items = append(items, PlaylistItem{Name: fmt.Sprintf("Channel-%d", i+1), URL: url})
		}
	case "remote":
		items = fetchRemotePlaylist(config.URL)
	case "local":
		items = readLocalPlaylist(config.Path)
	default:
		fmt.Println("Unknown type:", config.Type)
		return
	}

	if len(items) == 0 {
		fmt.Println("No URLs loaded.")
		return
	}

	urlsOnly := make([]string, len(items))
	for i, item := range items {
		urlsOnly[i] = item.URL
	}

	// 启动中断监听
	go handleInterrupt(items, len(items), "valid_urls.txt", "result.txt")

	// 开始并发扫描
	scanURLsConcurrently(urlsOnly, items)

	// 扫描完成后正常保存
	saveValidURLsToFile(validURLs, len(items), "valid_urls.txt", "result.txt")

}

func generateBlindURLs(expression string) []string {
	re := regexp.MustCompile(`\((\d+)-(\d+)\)`)
	matches := re.FindAllStringSubmatch(expression, -1)

	if len(matches) == 0 {
		fmt.Println("No range found in the expression.")
		return nil
	}

	var ranges [][3]int // start, end, maskWidth (0 表示不启用)
	for _, match := range matches {
		startStr, endStr := match[1], match[2]
		start, _ := strconv.Atoi(startStr)
		end, _ := strconv.Atoi(endStr)

		useMask := false
		width := len(startStr)

		// 满足条件：起始值与结束值位数一致，并且起始值以 0 开头
		if len(startStr) == len(endStr) && strings.HasPrefix(startStr, "0") && start <= end {
			useMask = true
		}

		if useMask {
			ranges = append(ranges, [3]int{start, end, width})
		} else {
			ranges = append(ranges, [3]int{start, end, 0}) // 0 表示不启用掩码
		}
	}

	return generateURLs(expression, re, ranges, 0)
}

func generateURLs(expression string, re *regexp.Regexp, ranges [][3]int, index int) []string {
	if index == len(ranges) {
		return []string{expression}
	}

	var urls []string
	start, end, maskWidth := ranges[index][0], ranges[index][1], ranges[index][2]

	for i := start; i <= end; i++ {
		var formattedNumber string
		if maskWidth > 0 && i < int(math.Pow10(maskWidth)) {
			formattedNumber = fmt.Sprintf("%0*d", maskWidth, i)
		} else {
			formattedNumber = strconv.Itoa(i)
		}

		expr := replaceFirstMatch(expression, re, formattedNumber)
		urls = append(urls, generateURLs(expr, re, ranges, index+1)...)
	}
	return urls
}

// 替换第一个匹配项
func replaceFirstMatch(s string, re *regexp.Regexp, replacement string) string {
	loc := re.FindStringIndex(s)
	if loc == nil {
		return s
	}
	return s[:loc[0]] + replacement + s[loc[1]:]
}

func fetchRemotePlaylist(url string) []PlaylistItem {
	resp, err := http.Get(url)
	if err != nil || resp.StatusCode != 200 {
		fmt.Println("Failed to fetch remote playlist:", err)
		return nil
	}
	defer resp.Body.Close()

	// 将整个响应体读入内存
	bodyBytes, err := io.ReadAll(resp.Body)
	if err != nil {
		fmt.Println("Failed to read response body:", err)
		return nil
	}

	// 构造一个内存中的 scanner
	scanner := bufio.NewScanner(bytes.NewReader(bodyBytes))

	// 检测格式
	format, err := detectRemoteSourceFormat(bytes.NewReader(bodyBytes))
	if err != nil {
		fmt.Println("Failed to detect playlist format:", err)
		return nil
	}

	// 再次构造 scanner（scanner 不可重置）
	scanner = bufio.NewScanner(bytes.NewReader(bodyBytes))
	items, err := parsePlaylist(scanner, format)
	if err != nil {
		fmt.Println("Error parsing playlist:", err)
		return nil
	}

	var playlistItems []PlaylistItem
	for _, item := range items {
		playlistItems = append(playlistItems, PlaylistItem{
			Name: item.Name,
			URL:  item.URL,
		})
	}

	return playlistItems
}

// detectRemoteSourceFormat 根据远程播放列表内容判断格式
func detectRemoteSourceFormat(reader io.Reader) (string, error) {
	scanner := bufio.NewScanner(reader)
	if scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if strings.HasPrefix(line, "#EXTM3U") {
			return ".m3u", nil
		} else if strings.HasPrefix(line, "[playlist]") {
			return ".pls", nil
		}
	}
	if err := scanner.Err(); err != nil {
		return "", err
	}
	return ".txt", nil
}

func readLocalPlaylist(path string) []PlaylistItem {
	file, err := os.Open(path)
	if err != nil {
		fmt.Println("Failed to open local playlist:", err)
		return nil
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)
	format, err := detectSourceFormat(path)
	if err != nil {
		return nil
	}
	items, err := parsePlaylist(scanner, format)
	if err != nil {
		fmt.Println("Error parsing playlist:", err)
		return nil
	}

	var playlistItems []PlaylistItem
	for _, item := range items {
		playlistItems = append(playlistItems, PlaylistItem{
			Name: item.Name,
			URL:  item.URL,
		})
	}

	return playlistItems
}

func parseM3UPlaylist(scanner *bufio.Scanner) []PlaylistItem {
	var items []PlaylistItem
	var currentName string

	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())

		if strings.HasPrefix(line, "#EXTINF:") {
			commaIndex := strings.LastIndex(line, ",")
			if commaIndex != -1 {
				currentName = line[commaIndex+1:]
			} else {
				currentName = "Unknown"
			}
		} else if isValidM3U8Line(line) {
			if currentName != "" {
				items = append(items, PlaylistItem{Name: currentName, URL: line})
				currentName = ""
			}
		}
	}
	return items
}

func isValidM3U8Line(line string) bool {
	return strings.HasPrefix(line, "http://") || strings.HasPrefix(line, "https://") || strings.HasPrefix(line, "rtsp://") || strings.HasPrefix(line, "rtp://")
}

func scanURLsConcurrently(urls []string, items []PlaylistItem) {
	var wg sync.WaitGroup
	urlChan := make(chan IndexedURL, urlBatch)
	validURLsChan := make(chan IndexedURL, len(urls)) // 缓冲足够大避免阻塞
	done := make(chan bool)

	totalURLs := len(urls)
	scannedCount := 0
	var scannedCountMutex sync.Mutex

	// Start workers
	for i := 0; i < numWorkers; i++ {
		wg.Add(1)
		go workerWithIndex(&wg, urlChan, validURLsChan, &scannedCount, &scannedCountMutex)
	}

	// Send URLs with index
	go func() {
		for i, url := range urls {
			urlChan <- IndexedURL{URL: url, Index: i}
		}
		close(urlChan)
	}()

	// Collect valid URLs
	var validIndexedURLs []IndexedURL
	go func() {
		for indexedURL := range validURLsChan {
			validIndexedURLs = append(validIndexedURLs, indexedURL)
		}
	}()

	// Wait for all workers to finish
	go func() {
		wg.Wait()
		close(validURLsChan)
		close(done)
	}()

	// Print progress
	go func() {
		ticker := time.NewTicker(1 * time.Second)
		defer ticker.Stop()
		for {
			select {
			case <-ticker.C:
				scannedCountMutex.Lock()
				scanned := scannedCount
				scannedCountMutex.Unlock()
				percentage := float64(scanned) / float64(totalURLs) * 100
				fmt.Printf("Scanned: %d/%d (%.2f%%)\n", scanned, totalURLs, percentage)
			case <-done:
				return
			}
		}
	}()

	// 等待完成或中断
	<-done

	// 提取有效源
	sort.Slice(validIndexedURLs, func(i, j int) bool {
		return validIndexedURLs[i].Index < validIndexedURLs[j].Index
	})

	validURLsMutex.Lock()
	defer validURLsMutex.Unlock()

	for _, indexedURL := range validIndexedURLs {
		for _, item := range items {
			if item.URL == indexedURL.URL {
				validURLs = append(validURLs, item)
				break
			}
		}
	}
}

func workerWithIndex(wg *sync.WaitGroup, urlChan chan IndexedURL, validURLsChan chan IndexedURL, scannedCount *int, scannedCountMutex *sync.Mutex) {
	defer wg.Done()

	for indexedURL := range urlChan {
		if isValidURL(indexedURL.URL) {
			validURLsChan <- indexedURL
		}
		scannedCountMutex.Lock()
		*scannedCount++
		scannedCountMutex.Unlock()
	}
}

func handleInterrupt(items []PlaylistItem, totalURLs int, validFilename, resultFilename string) {
	c := make(chan os.Signal, 1)
	signal.Notify(c, os.Interrupt, syscall.SIGTERM)

	<-c
	fmt.Println("\nInterrupt received, saving current results...")

	// 从全局变量获取当前有效 URL
	validURLsMutex.Lock()
	defer validURLsMutex.Unlock()

	saveValidURLsToFile(validURLs, totalURLs, validFilename, resultFilename)
	fmt.Println("Scan interrupted and current results saved.")
	os.Exit(0)
}

func worker(wg *sync.WaitGroup, urlChan chan string, validURLsChan chan string, done chan bool, scannedCount *int, scannedCountMutex *sync.Mutex) {
	defer wg.Done()

	for url := range urlChan {
		if isValidURL(url) {
			validURLsChan <- url
		}
		scannedCountMutex.Lock()
		*scannedCount++
		scannedCountMutex.Unlock()
	}
}

// isValidURL 判断 URL 是否有效（HTTP 或 RTSP）
func isValidURL(url string) bool {
	if strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") {
		return isValidHTTP(url)
	} else if strings.HasPrefix(url, "rtsp://") {
		return isValidRTSP(url)
	} else if strings.HasPrefix(url, "udp://") || strings.HasPrefix(url, "rtp://") {
		return isValidMulticast(url)
	}
	return false
}

func isValidMulticast(url string) bool {
	// 去除 rtp:// 或 udp:// 前缀
	addr := strings.TrimPrefix(url, "rtp://")
	addr = strings.TrimPrefix(addr, "udp://")

	// 去除参数部分（如 ?pkt_size=1316）
	if idx := strings.Index(addr, "?"); idx != -1 {
		addr = addr[:idx]
	}

	// 获取本机默认网卡（可自定义）
	iface, err := getDefaultInterface()
	if err != nil {
		fmt.Println("无法获取默认网络接口:", err)
		return false
	}

	// 设置最大等待时间（毫秒）
	const multicastDelay = 5000

	return isPortOpen(addr, iface, multicastDelay)
}

func getDefaultInterface() (*net.Interface, error) {
	ifaces, err := net.Interfaces()
	if err != nil {
		return nil, err
	}

	for _, iface := range ifaces {
		if (iface.Flags&net.FlagUp) != 0 && (iface.Flags&net.FlagLoopback) == 0 {
			return &iface, nil
		}
	}

	return nil, fmt.Errorf("no suitable network interface found")
}

func isPortOpen(addrport string, iface *net.Interface, multicastDelay int) bool {
	log.Printf("正在检查组播地址 %s 是否开放", addrport)

	host, portStr, err := net.SplitHostPort(addrport)
	if err != nil {
		log.Printf("Error parsing addrport: %v", err)
		return false
	}

	_, err = net.LookupPort("udp", portStr)
	if err != nil {
		log.Printf("Error parsing port: %v", err)
		return false
	}

	udpAddr, err := net.ResolveUDPAddr("udp", addrport)
	if err != nil {
		log.Printf("Error resolving UDP address: %v", err)
		return false
	}

	conn, err := net.ListenUDP("udp", udpAddr)
	if err != nil {
		log.Printf("Error listening UDP: %v", err)
		return false
	}
	defer conn.Close()

	pconn := ipv4.NewPacketConn(conn)
	group := net.ParseIP(host)
	if group.To4() == nil {
		log.Printf("Invalid multicast group IP: %v", host)
		return false
	}

	err = pconn.JoinGroup(iface, &net.UDPAddr{IP: group})
	if err != nil {
		log.Printf("Error joining multicast group: %v", err)
		return false
	}
	defer pconn.LeaveGroup(iface, &net.UDPAddr{IP: group})

	conn.SetReadDeadline(time.Now().Add(time.Duration(multicastDelay) * time.Millisecond))

	buffer := make([]byte, 8192)
	n, src, err := conn.ReadFromUDP(buffer)
	if err != nil {
		log.Printf("Error reading data: %v", err)
		return false
	}

	log.Printf("Received message from %s: %s\n", src, string(buffer[:n]))
	return true
}

// HTTP 检测保持不变
func isValidHTTP(url string) bool {
	resp, err := http.Get(url)
	if err != nil {
		return false
	}
	defer resp.Body.Close()
	return resp.StatusCode == 200
}

func isValidRTSP(url string) bool {
	return isValidRTSPWithRedirect(url, 3)
}

// 带重定向支持的 RTSP 检测
func isValidRTSPWithRedirect(url string, maxRedirects int) bool {
	if maxRedirects <= 0 {
		fmt.Println("Max redirects reached for", url)
		return false
	}

	u, err := parseRTSPURL(url)
	if err != nil {
		return false
	}

	conn, err := net.Dial("tcp", u.Host+":"+u.Port)
	if err != nil {
		return false
	}
	defer conn.Close()

	// Step 1: 发送 OPTIONS 请求
	optionsRequest := fmt.Sprintf("OPTIONS %s RTSP/1.0\r\nCSeq: 1\r\nUser-Agent: Lavf61.9.107\r\nHost: %s\r\n\r\n",
		u.Path, u.Host)

	_, err = conn.Write([]byte(optionsRequest))
	if err != nil {
		return false
	}

	// 等待 OPTIONS 响应
	buffer := make([]byte, 4096)
	n, err := readResponse(conn, buffer)
	if err != nil {
		return false
	}

	response := string(buffer[:n])

	// 检查是否是 302 重定向
	if strings.Contains(response, "RTSP/1.0 302") {
		redirectURL := extractLocationHeader(response)
		if redirectURL == "" {
			fmt.Println("302 response but no Location header found")
			return false
		}

		// 构造完整 URL（如果 Location 是相对路径）
		newURL, err := resolveRelativeRTSP(url, redirectURL)
		if err != nil {
			fmt.Println("Failed to resolve redirect URL:", err)
			return false
		}

		fmt.Printf("Redirecting to: %s\n", newURL)
		return isValidRTSPWithRedirect(newURL, maxRedirects-1)
	}

	if !strings.Contains(response, "RTSP/1.0 200 OK") {
		return false
	}

	// Step 2: 发送 DESCRIBE 请求
	describeRequest := fmt.Sprintf("DESCRIBE %s RTSP/1.0\r\nCSeq: 2\r\nUser-Agent: Lavf61.9.107\r\nAccept: application/sdp\r\nHost: %s\r\n\r\n",
		u.Path, u.Host)

	_, err = conn.Write([]byte(describeRequest))
	if err != nil {
		return false
	}

	// 等待 DESCRIBE 响应
	n, err = readResponse(conn, buffer)
	if err != nil {
		return false
	}

	response = string(buffer[:n])
	if !strings.Contains(response, "Content-Type: application/sdp") {
		return false
	}

	// 提取 SDP 数据并判断是否非空
	sdpStart := strings.Index(response, "\r\n\r\n")
	if sdpStart == -1 || len(response) <= sdpStart+4 {
		return false
	}

	sdpData := response[sdpStart+4:]
	return len(strings.TrimSpace(sdpData)) > 0
}

// 提取 RTSP 响应中的 Location 头
func extractLocationHeader(response string) string {
	lines := strings.Split(response, "\r\n")
	for _, line := range lines {
		if strings.HasPrefix(line, "Location:") {
			parts := strings.SplitN(line, ":", 2)
			if len(parts) == 2 {
				return strings.TrimSpace(parts[1])
			}
		}
	}
	return ""
}

// 将相对路径转换为完整 RTSP URL
func resolveRelativeRTSP(baseURL, location string) (string, error) {
	baseURL = strings.TrimSuffix(baseURL, "/")

	if strings.HasPrefix(location, "rtsp://") {
		return location, nil
	}

	if strings.HasPrefix(location, "/") {
		u, err := parseRTSPURL(baseURL)
		if err != nil {
			return "", err
		}
		return fmt.Sprintf("rtsp://%s:%s%s", u.Host, u.Port, location), nil
	}

	// 相对路径，例如 ../ 或者直接文件名
	baseDir := baseURL[:strings.LastIndex(baseURL, "/")+1]
	return baseDir + location, nil
}

// 读取完整响应或超时
func readResponse(conn net.Conn, buffer []byte) (int, error) {
	_ = conn.SetReadDeadline(time.Now().Add(5 * time.Second))
	n, err := conn.Read(buffer)
	if err != nil {
		return n, err
	}

	// 如果响应不完整，继续读取直到遇到 "\r\n\r\n" 结束标志
	if !strings.Contains(string(buffer[:n]), "\r\n\r\n") {
		for {
			_ = conn.SetReadDeadline(time.Now().Add(2 * time.Second))
			more, err := conn.Read(buffer[n:])
			if err != nil {
				break
			}
			n += more
			if strings.Contains(string(buffer[:n]), "\r\n\r\n") {
				break
			}
		}
	}
	return n, nil
}

// 解析 RTSP URL
type rtspURL struct {
	Host   string
	Port   string
	Path   string
	Scheme string
}

func parseRTSPURL(rawURL string) (*rtspURL, error) {
	var u rtspURL
	if !strings.HasPrefix(rawURL, "rtsp://") {
		return nil, fmt.Errorf("not a rtsp url")
	}

	hostPath := strings.TrimPrefix(rawURL, "rtsp://")
	slashIndex := strings.Index(hostPath, "/")
	if slashIndex == -1 {
		slashIndex = len(hostPath)
	}

	hostPort := hostPath[:slashIndex]
	path := "/"
	if slashIndex < len(hostPath) {
		path += hostPath[slashIndex+1:]
	}

	port := "554" // 默认 RTSP 端口
	colonIndex := strings.Index(hostPort, ":")
	if colonIndex != -1 {
		hostPortSplit := strings.Split(hostPort, ":")
		hostPort = hostPortSplit[0]
		port = hostPortSplit[1]
	}

	u.Host = hostPort
	u.Port = port
	u.Path = path
	u.Scheme = "rtsp"

	return &u, nil
}

func saveValidURLsToFile(validItems []PlaylistItem, totalURLs int, validFilename, resultFilename string) {
	file, err := os.Create(validFilename)
	if err != nil {
		fmt.Println("Error creating file:", err)
		return
	}
	defer file.Close()

	for _, item := range validItems {
		_, err := file.WriteString(fmt.Sprintf("%s,%s\n", item.Name, item.URL))
		if err != nil {
			fmt.Println("Error writing to file:", err)
			return
		}
	}
	fmt.Println("Valid URLs saved to", validFilename)

	resultFile, err := os.Create(resultFilename)
	if err != nil {
		fmt.Println("Error creating result file:", err)
		return
	}
	defer resultFile.Close()

	validCount := len(validItems)

	_, err = resultFile.WriteString(fmt.Sprintf("总源数量: %d\n有效源数量: %d\n", totalURLs, validCount))
	if err != nil {
		fmt.Println("Error writing to result file:", err)
		return
	}

	fmt.Println("Result saved to", resultFilename)
}

// parsePlaylist 解析 txt/m3u/pls 格式的播放列表并返回 PlaylistItem 列表
// parsePlaylist 解析播放列表文件，根据文件类型调用对应的解析函数
func parsePlaylist(scanner *bufio.Scanner, format string) ([]PlaylistItem, error) {
	switch format {
	case ".txt":
		return parseTXT(scanner)
	case ".m3u":
		return parseM3U(scanner)
	case ".pls":
		return parsePLS(scanner)
	default:
		return nil, fmt.Errorf("unsupported playlist format: %s", format)
	}
}

// parseTXT 解析 .txt 格式的播放列表
func parseTXT(scanner *bufio.Scanner) ([]PlaylistItem, error) {
	var items []PlaylistItem
	var currentGroup string

	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if line == "" {
			continue
		}

		// TXT 分组标记
		if strings.HasSuffix(line, "#genre#") {
			currentGroup = strings.TrimSuffix(line, ",#genre#")
			continue
		}

		// 名称和 URL 对
		parts := strings.SplitN(line, ",", 2)
		if len(parts) == 2 {
			items = append(items, PlaylistItem{
				Name:  parts[0],
				URL:   parts[1],
				Group: currentGroup,
			})
			currentGroup = ""
		}
	}

	if err := scanner.Err(); err != nil {
		return nil, err
	}

	return items, nil
}

// parseM3U 解析 .m3u 格式的播放列表
func parseM3U(scanner *bufio.Scanner) ([]PlaylistItem, error) {
	var items []PlaylistItem
	var currentName string
	var currentGroup string

	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())

		if strings.HasPrefix(line, "#EXTINF:") {
			// 使用正则提取 group-title
			re := regexp.MustCompile(`group-title="([^"]+)"`)
			groupMatch := re.FindStringSubmatch(line)
			if len(groupMatch) > 1 {
				currentGroup = groupMatch[1]
			}

			commaIndex := strings.LastIndex(line, ",")
			if commaIndex != -1 && commaIndex < len(line)-1 {
				currentName = line[commaIndex+1:]
			} else {
				currentName = "Unknown"
			}
		} else if isValidM3U8Line(line) && currentName != "" {
			items = append(items, PlaylistItem{
				Name:  currentName,
				URL:   line,
				Group: currentGroup,
			})
			currentName = ""
			currentGroup = ""
		}
	}

	if err := scanner.Err(); err != nil {
		return nil, err
	}

	return items, nil
}

// parsePLS 解析 .pls 格式的播放列表
func parsePLS(scanner *bufio.Scanner) ([]PlaylistItem, error) {
	var items []PlaylistItem
	var currentTitle string

	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if line == "" {
			continue
		}

		if strings.HasPrefix(line, "[playlist]") || strings.HasPrefix(line, "NumberOfEntries=") || strings.HasPrefix(line, "Version=") {
			continue
		}

		if strings.HasPrefix(line, "File") {
			fileParts := strings.SplitN(line, "=", 2)
			if len(fileParts) == 2 {
				url := fileParts[1]

				// 获取标题行
				if scanner.Scan() && strings.HasPrefix(scanner.Text(), "Title") {
					titleParts := strings.SplitN(scanner.Text(), "=", 2)
					if len(titleParts) == 2 {
						currentTitle = titleParts[1]
					}
				}

				name := currentTitle
				if name == "" {
					name = url
				}

				items = append(items, PlaylistItem{
					Name: name,
					URL:  url,
				})

				currentTitle = ""
			}
		}
	}

	if err := scanner.Err(); err != nil {
		return nil, err
	}

	return items, nil
}

func detectSourceFormat(filePath string) (string, error) {
	file, err := os.Open(filePath)
	if err != nil {
		return "", err
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)
	if scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if strings.HasPrefix(line, "#EXTM3U") {
			return ".m3u", nil
		} else if strings.HasPrefix(line, "[playlist]") {
			return ".pls", nil
		}
	}
	if err := scanner.Err(); err != nil {
		return "", err
	}

	// 默认认为是 .txt
	return ".txt", nil
}
